Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / service / src / commonTest / kotlin / org / meshtastic / core / service / MeshServiceOrchestratorTest.kt

Displaying Raw • Download

core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt 4d4070c8e1ed73a56ee2f054300a0f62e3fe2e62 (4d4070c8) Text, 27.97 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service

Tff7b72import T7ee787co.touchlab.kermit.Severity
Tff7b72import T7ee787dev.mokkery.MockMode
Tff7b72import T7ee787dev.mokkery.answering.calls
Tff7b72import T7ee787dev.mokkery.answering.returns
Tff7b72import T7ee787dev.mokkery.every
Tff7b72import T7ee787dev.mokkery.everySuspend
Tff7b72import T7ee787dev.mokkery.matcher.any
Tff7b72import T7ee787dev.mokkery.mock
Tff7b72import T7ee787dev.mokkery.verify
Tff7b72import T7ee787dev.mokkery.verify.VerifyMode.Companion.atLeast
Tff7b72import T7ee787dev.mokkery.verify.VerifyMode.Companion.exactly
Tff7b72import T7ee787dev.mokkery.verifySuspend
Tff7b72import T7ee787kotlinx.coroutines.ExperimentalCoroutinesApi
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableSharedFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.test.UnconfinedTestDispatcher
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787okio.ByteString.Companion.toByteString
Tff7b72import T7ee787org.meshtastic.core.common.database.DatabaseManager
Tff7b72import T7ee787org.meshtastic.core.common.util.safeCatchingAll
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.repository.CommandSender
Tff7b72import T7ee787org.meshtastic.core.repository.MeshConfigHandler
Tff7b72import T7ee787org.meshtastic.core.repository.MeshConnectionManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshMessageProcessor
Tff7b72import T7ee787org.meshtastic.core.repository.MeshNotificationManager
Tff7b72import T7ee787org.meshtastic.core.repository.NodeManager
Tff7b72import T7ee787org.meshtastic.core.repository.NodeRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioSessionContext
Tff7b72import T7ee787org.meshtastic.core.repository.ReceivedRadioFrame
Tff7b72import T7ee787org.meshtastic.core.repository.ServiceRepository
Tff7b72import T7ee787org.meshtastic.core.repository.TakPrefs
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.local_network_permission_denied_hint
Tff7b72import T7ee787org.meshtastic.core.takserver.MeshToCotBroadcaster
Tff7b72import T7ee787org.meshtastic.core.takserver.TAKMeshIntegration
Tff7b72import T7ee787org.meshtastic.core.takserver.TAKServerManager
Tff7b72import T7ee787org.meshtastic.proto.FromRadio
Tff7b72import T7ee787org.meshtastic.proto.LocalModuleConfig
Tff7b72import T7ee787org.meshtastic.proto.MyNodeInfo
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertTrue

Tff7b72class T56d364MeshServiceOrchestratorTest Tb4b4b4{

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3DEFAULT_ADDRESS Tff7b72= Ta5d6ff"Ta5d6ffx:AA:BB:CC:DD:EE:FFTa5d6ff"
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceService Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3serviceRepositoryTb4b4b4: Te6edf3ServiceRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3nodeManagerTb4b4b4: Te6edf3NodeManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

Tff7b72private Tff7b72val Te6edf3messageProcessorTb4b4b4: Te6edf3MeshMessageProcessor Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3commandSenderTb4b4b4: Te6edf3CommandSender Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3meshConfigHandlerTb4b4b4: Te6edf3MeshConfigHandler Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3serviceNotificationsTb4b4b4: Te6edf3MeshNotificationManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3takServerManagerTb4b4b4: Te6edf3TAKServerManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3takPrefsTb4b4b4: Te6edf3TakPrefs Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3nodeRepositoryTb4b4b4: Te6edf3NodeRepository Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3databaseManagerTb4b4b4: Te6edf3DatabaseManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)
Tff7b72private Tff7b72val Te6edf3connectionManagerTb4b4b4: Te6edf3MeshConnectionManager Tff7b72= Te6edf3mockTb4b4b4(Te6edf3MockModeTb4b4b4.Te6edf3autofillTb4b4b4)

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72private Tff7b72val Te6edf3testDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)

Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72private Tff7b72val Te6edf3dispatchers Tff7b72= Te6edf3CoroutineDispatchersTb4b4b4(Te6edf3io Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3main Tff7b72= Te6edf3testDispatcherTb4b4b4, Te6edf3default Tff7b72= Te6edf3testDispatcherTb4b4b4)

T8b949e/** Stubs the shared flow dependencies used by every test and returns an orchestrator. */
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalCoroutinesApiTff7b72::Te6edf3classTb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffcreateOrchestratorTb4b4b4(
Te6edf3receivedDataTb4b4b4: Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72> Tff7b72= Te6edf3MutableSharedFlowTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3connectionErrorTb4b4b4: Te6edf3MutableSharedFlowTff7b72<Tffa657StringTff7b72> Tff7b72= Te6edf3MutableSharedFlowTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3connectionStateTb4b4b4: Te6edf3MutableStateFlowTff7b72<Te6edf3ConnectionStateTff7b72> Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)Tb4b4b4,
T8b949e// A valid default address lets every start() proceed through the new
T8b949e// wait-for-valid-address -> DB switch -> connect ordering without per-test boilerplate.
T8b949e// Tests that need a different initial address (or no address) override it explicitly.
Te6edf3currentDeviceAddressFlowTb4b4b4: Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4,
Te6edf3sessionGenerationTb4b4b4: Te6edf3MutableStateFlowTff7b72<Tffa657LongTff7b72> Tff7b72= Te6edf3MutableStateFlowTb4b4b4(T79c0ff0LTb4b4b4)Tb4b4b4,
Te6edf3activeSessionTb4b4b4: Te6edf3MutableStateFlowTff7b72<Te6edf3RadioSessionContext?Tff7b72> Tff7b72=
Te6edf3MutableStateFlowTb4b4b4(Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3valueTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3RadioSessionContextTb4b4b4(Te6edf3sessionGenerationTb4b4b4.Te6edf3valueTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3isSessionActiveTb4b4b4: Tb4b4b4(Te6edf3RadioSessionContextTb4b4b4) Tff7b72-Tff7b72> Tffa657Boolean Tff7b72= Tb4b4b4{ Te6edf3activeSessionTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Tffa657it Tb4b4b4}Tb4b4b4,
Te6edf3takEnabledFlowTb4b4b4: Te6edf3MutableStateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)Tb4b4b4,
Te6edf3takRunningFlowTb4b4b4: Te6edf3MutableStateFlowTff7b72<Tffa657BooleanTff7b72> Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)Tb4b4b4,
T8b949e// Granted by default so every pre-existing test keeps reaching connect(); DEFAULT_ADDRESS is BLE
T8b949e// anyway, but the cold-start ordering test deliberately uses a TCP address.
Te6edf3localNetworkAccessTb4b4b4: Te6edf3LocalNetworkAccess Tff7b72= Te6edf3LocalNetworkAccess Tb4b4b4{ Tff7b72true Tb4b4b4}Tb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3MeshServiceOrchestrator Tb4b4b4{
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3receivedData Tb4b4b4} Te6edf3returns Te6edf3receivedData
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectionError Tb4b4b4} Te6edf3returns Te6edf3connectionError
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectionState Tb4b4b4} Te6edf3returns Te6edf3connectionState
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3currentDeviceAddressFlow Tb4b4b4} Te6edf3returns Te6edf3currentDeviceAddressFlow
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3sessionGeneration Tb4b4b4} Te6edf3returns Te6edf3sessionGeneration
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3activeSession Tb4b4b4} Te6edf3returns Te6edf3activeSession
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3isSessionActiveTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3isSessionActiveTb4b4b4(Tffa657itTb4b4b4.Te6edf3argsTff7b72[T79c0ff0Tff7b72] Tff7b72as Te6edf3RadioSessionContextTb4b4b4)
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3meshPacketFlow Tb4b4b4} Te6edf3returns Te6edf3MutableSharedFlowTb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3meshConfigHandlerTb4b4b4.Te6edf3moduleConfig Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Te6edf3LocalModuleConfigTb4b4b4(Tb4b4b4)Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3takPrefsTb4b4b4.Te6edf3isTakServerEnabled Tb4b4b4} Te6edf3returns Te6edf3takEnabledFlow
Te6edf3every Tb4b4b4{ Te6edf3takPrefsTb4b4b4.Te6edf3isMeshToCotEnabled Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3takPrefsTb4b4b4.Te6edf3takServerChannel Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(T79c0ff0Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3isRunning Tb4b4b4} Te6edf3returns Te6edf3takRunningFlow
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3inboundMessages Tb4b4b4} Te6edf3returns Te6edf3MutableSharedFlowTb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeRepositoryTb4b4b4.Te6edf3myNodeInfo Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)

T8b949e// Deliberately its own dispatcher, not the class-level testDispatcher: the broadcaster's
T8b949e// scheduler doesn't need to be the same one driving this test, and a distinct name keeps
T8b949e// that from reading as though the two are linked.
Tff7b72val Te6edf3broadcasterTestDispatcher Tff7b72= Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3takMeshIntegration Tff7b72=
Te6edf3TAKMeshIntegrationTb4b4b4(
Te6edf3takServerManager Tff7b72= Te6edf3takServerManagerTb4b4b4,
Te6edf3commandSender Tff7b72= Te6edf3commandSenderTb4b4b4,
Te6edf3serviceRepository Tff7b72= Te6edf3serviceRepositoryTb4b4b4,
Te6edf3meshConfigHandler Tff7b72= Te6edf3meshConfigHandlerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3takPrefs Tff7b72= Te6edf3takPrefsTb4b4b4,
Te6edf3meshToCotBroadcaster Tff7b72=
Te6edf3MeshToCotBroadcasterTb4b4b4(
Te6edf3takServerManager Tff7b72= Te6edf3takServerManagerTb4b4b4,
Te6edf3nodeRepository Tff7b72= Te6edf3nodeRepositoryTb4b4b4,
Te6edf3takPrefs Tff7b72= Te6edf3takPrefsTb4b4b4,
Te6edf3dispatchers Tff7b72=
Te6edf3CoroutineDispatchersTb4b4b4(
Te6edf3io Tff7b72= Te6edf3broadcasterTestDispatcherTb4b4b4,
Te6edf3main Tff7b72= Te6edf3broadcasterTestDispatcherTb4b4b4,
Te6edf3default Tff7b72= Te6edf3broadcasterTestDispatcherTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tff7b72return Te6edf3MeshServiceOrchestratorTb4b4b4(
Te6edf3radioInterfaceService Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4,
Te6edf3serviceStateWriter Tff7b72= Te6edf3serviceRepositoryTb4b4b4,
Te6edf3nodeManager Tff7b72= Te6edf3nodeManagerTb4b4b4,
Te6edf3messageProcessor Tff7b72= Te6edf3messageProcessorTb4b4b4,
Te6edf3serviceNotifications Tff7b72= Te6edf3serviceNotificationsTb4b4b4,
Te6edf3takServerManager Tff7b72= Te6edf3takServerManagerTb4b4b4,
Te6edf3takMeshIntegration Tff7b72= Te6edf3takMeshIntegrationTb4b4b4,
Te6edf3takPrefs Tff7b72= Te6edf3takPrefsTb4b4b4,
Te6edf3databaseManager Tff7b72= Te6edf3databaseManagerTb4b4b4,
Te6edf3connectionManager Tff7b72= Te6edf3connectionManagerTb4b4b4,
Te6edf3dispatchers Tff7b72= Te6edf3dispatchersTb4b4b4,
Te6edf3localNetworkAccess Tff7b72= Te6edf3localNetworkAccessTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffframeTb4b4b4(
Te6edf3bytesTb4b4b4: Te6edf3ByteArrayTb4b4b4,
Te6edf3sessionTb4b4b4: Te6edf3RadioSessionContext Tff7b72= Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff0LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4,
Tb4b4b4) Tff7b72= Te6edf3ReceivedRadioFrameTb4b4b4(Te6edf3bytesTb4b4b4.Te6edf3toByteStringTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3sessionTb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8fftestStartWiresComponentsTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Tb4b4b4)

Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3serviceNotificationsTb4b4b4.Te6edf3initChannelsTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestTakServerStartsAndStopsWithPreferenceTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3takEnabledFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3takRunningFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)

Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3takEnabledFlow Tff7b72= Te6edf3takEnabledFlowTb4b4b4, Te6edf3takRunningFlow Tff7b72= Te6edf3takRunningFlowTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

T8b949e// Toggle on
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3verify Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3startTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}

T8b949e// Update mock state to reflect it's running
Te6edf3takRunningFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72true

T8b949e// Toggle off
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3verify Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestTakServerCanRetryAfterFailedStartTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3takEnabledFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3takRunningFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3lifecycleEvents Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3startTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3lifecycleEvents Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstartTa5d6ff"
Tffa657Unit
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3lifecycleEvents Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstopTa5d6ff"
Tffa657Unit
Tb4b4b4}
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3takEnabledFlow Tff7b72= Te6edf3takEnabledFlowTb4b4b4, Te6edf3takRunningFlow Tff7b72= Te6edf3takRunningFlowTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
T8b949e// The mock never changes takRunningFlow, modeling a start attempt that failed before listening.
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72true

Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstopTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3lifecycleEventsTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstopTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstopTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3lifecycleEventsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestStopStopsTakServerWhileStartingTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3takEnabledFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72trueTb4b4b4)
Tff7b72val Te6edf3takRunningFlow Tff7b72= Te6edf3MutableStateFlowTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72val Te6edf3lifecycleEvents Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3startTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3lifecycleEvents Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstartTa5d6ff"
Tffa657Unit
Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3takServerManagerTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls
Tb4b4b4{
Te6edf3lifecycleEvents Tff7b72+Tff7b72= Ta5d6ff"Ta5d6ffstopTa5d6ff"
Tffa657Unit
Tb4b4b4}
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3takEnabledFlow Tff7b72= Te6edf3takEnabledFlowTb4b4b4, Te6edf3takRunningFlow Tff7b72= Te6edf3takRunningFlowTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)

Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72false
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3takEnabledFlowTb4b4b4.Te6edf3value Tff7b72= Tff7b72true
Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstopTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstartTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffstopTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3lifecycleEventsTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestStartCallsSwitchActiveDatabaseTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// New ordering: start() waits for currentDeviceAddressFlow to surface a valid address,
T8b949e// then switches the DB to it and connects. getDeviceAddress() is no longer consulted
T8b949e// from start(), so drive the address via the flow.
Tff7b72val Te6edf3deviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Ta5d6ff"Ta5d6fftcp:192.168.1.100Ta5d6ff"Tb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3currentDeviceAddressFlow Tff7b72= Te6edf3deviceAddressFlowTb4b4b4)

T8b949e// Event recorder: locks the cold-start ordering (DB switch -> load cached NodeDB ->
T8b949e// connect) without depending on Mokkery's global call-order semantics. The global
T8b949e// order verifier (verifySuspend(order) { ... }) fails because start() also drives
T8b949e// other calls on the same mocks (resetReceivedBuffer, currentDeviceAddressFlow
T8b949e// collectors, the mid-session DB switch replay, etc.) that interleave with the
T8b949e// three calls we care about and break the global sequence. Recording only the
T8b949e// three calls under test sidesteps the global check entirely.
Tff7b72val Te6edf3events Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657StringTff7b72>Tb4b4b4(Tb4b4b4)
Te6edf3everySuspend Tb4b4b4{ Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4} Te6edf3calls Tb4b4b4{ Te6edf3eventsTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffswitchDbTa5d6ff"Tb4b4b4) Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls Tb4b4b4{ Te6edf3eventsTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffloadCachedNodeDbTa5d6ff"Tb4b4b4) Tb4b4b4}
Te6edf3every Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4) Tb4b4b4} Te6edf3calls Tb4b4b4{ Te6edf3eventsTb4b4b4.Te6edf3addTb4b4b4(Ta5d6ff"Ta5d6ffconnectTa5d6ff"Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

Te6edf3verifySuspend Tb4b4b4{ Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6fftcp:192.168.1.100Ta5d6ff"Tb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4) Tb4b4b4}

T8b949e// Locks in the cold-start ordering invariant: DB switch -> load cached NodeDB ->
T8b949e// connect. loadCachedNodeDB must run AFTER the DB switch (so it reads from the
T8b949e// freshly-selected per-device DB) and BEFORE connect() (so the firmware handshake
T8b949e// doesn't see a stale or empty node set). On UnconfinedTestDispatcher the
T8b949e// handledLaunch block runs eagerly, producing these three events in order. The
T8b949e// mid-session address observer then replays the initial value, producing a
T8b949e// redundant "switchDb"; assert only the first three to lock the order without
T8b949e// coupling to the redundant replay.
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Ta5d6ff"Ta5d6ffswitchDbTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffloadCachedNodeDbTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffconnectTa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3eventsTb4b4b4.Te6edf3takeTb4b4b4(T79c0ff3Tb4b4b4)Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestConnectionErrorForwardedToServiceRepositoryTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3connectionError Tff7b72= Te6edf3MutableSharedFlowTff7b72<Tffa657StringTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff1Tb4b4b4)

Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3connectionError Tff7b72= Te6edf3connectionErrorTb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

T8b949e// Emit an error into the radio interface's connectionError flow
Te6edf3connectionErrorTb4b4b4.Te6edf3tryEmitTb4b4b4(Ta5d6ff"Ta5d6ffBLE connection lostTa5d6ff"Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Ta5d6ff"Ta5d6ffBLE connection lostTa5d6ff"Tb4b4b4, Te6edf3SeverityTb4b4b4.Te6edf3WarnTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftestStartIsIdempotentTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)

T8b949e// Second call should be a no-op
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)

T8b949e// Components should only be initialized once
Te6edf3verifyTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3serviceNotificationsTb4b4b4.Te6edf3initChannelsTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verifyTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3loadCachedNodeDBTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)
Tb4b4b4}

T8b949e/**
* Regression test for a bug where `stop()` did not actually tear down the FromRadio collectors. Collectors were
* attached to an injected process-wide ServiceScope rather than a per-start scope, so `start() -> stop() ->
* start()` caused duplicate collectors and every FromRadio packet was handled 2x (then 3x, etc.).
*/
Tf0883e@Test
Tff7b72fun Td2a8fftestFromRadioCollectorsTornDownOnStopAndRestartedCleanlyOnStartTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff8Tb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3receivedData Tff7b72= Te6edf3receivedDataTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3packet1 Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff2Tb4b4b4, T79c0ff3Tb4b4b4)Tb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3packet1Tb4b4b4)
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packet1Tb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3packet2 Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff4Tb4b4b4, T79c0ff5Tb4b4b4, T79c0ff6Tb4b4b4)Tb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3packet2Tb4b4b4)
T8b949e// After stop(), the collector must be gone - the handler should not be invoked for packet2.
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packet2Tb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3packet3 Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff7Tb4b4b4, T79c0ff8Tb4b4b4, T79c0ff9Tb4b4b4)Tb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3packet3Tb4b4b4)
T8b949e// After restart, a single fresh collector must process packet3 exactly once (not twice).
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packet3Tb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffframeIsDiscardedAfterAdmissionClosesEvenWhileLifecycleTokenIsDrainingTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff1Tb4b4b4)
Tff7b72val Te6edf3session Tff7b72= Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff1LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)
Tff7b72val Te6edf3activeSession Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3RadioSessionContext?Tff7b72>Tb4b4b4(Te6edf3sessionTb4b4b4)
Tff7b72var Te6edf3admissionOpen Tff7b72= Tff7b72true
Tff7b72val Te6edf3orchestrator Tff7b72=
Te6edf3createOrchestratorTb4b4b4(
Te6edf3receivedData Tff7b72= Te6edf3receivedDataTb4b4b4,
Te6edf3sessionGeneration Tff7b72= Te6edf3MutableStateFlowTb4b4b4(T79c0ff1LTb4b4b4)Tb4b4b4,
Te6edf3activeSession Tff7b72= Te6edf3activeSessionTb4b4b4,
Te6edf3isSessionActive Tff7b72= Tb4b4b4{ Te6edf3admissionOpen Tff7b72&Tff7b72& Te6edf3activeSessionTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Tffa657it Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3frame Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff1Tb4b4b4, T79c0ff2Tb4b4b4, T79c0ff3Tb4b4b4)Tb4b4b4, Te6edf3sessionTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3admissionOpen Tff7b72= Tff7b72false
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3frameTb4b4b4)

Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3frameTb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3sessionTb4b4b4, Te6edf3activeSessionTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffthe lifecycle token may remain published while leases drainTa5d6ff"Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffqueuedMyNodeInfoFromOldSessionIsDiscardedAfterSameAddressReconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff2Tb4b4b4)
Tff7b72val Te6edf3activeGeneration Tff7b72= Te6edf3MutableStateFlowTb4b4b4(T79c0ff1LTb4b4b4)
Tff7b72val Te6edf3activeSession Tff7b72=
Te6edf3MutableStateFlowTff7b72<Te6edf3RadioSessionContext?Tff7b72>Tb4b4b4(Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff1LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72=
Te6edf3createOrchestratorTb4b4b4(
Te6edf3receivedData Tff7b72= Te6edf3receivedDataTb4b4b4,
Te6edf3sessionGeneration Tff7b72= Te6edf3activeGenerationTb4b4b4,
Te6edf3activeSession Tff7b72= Te6edf3activeSessionTb4b4b4,
Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3payload Tff7b72= Te6edf3FromRadioTb4b4b4(Te6edf3my_info Tff7b72= Te6edf3MyNodeInfoTb4b4b4(Te6edf3my_node_num Tff7b72= T79c0ff4T79c0ff2Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3encodeTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3staleFrame Tff7b72= Te6edf3frameTb4b4b4(Te6edf3payloadTb4b4b4, Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff1LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3freshFrame Tff7b72= Te6edf3frameTb4b4b4(Te6edf3payloadTb4b4b4, Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff2LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3activeGenerationTb4b4b4.Te6edf3value Tff7b72= T79c0ff2L
Te6edf3activeSessionTb4b4b4.Te6edf3value Tff7b72= Te6edf3RadioSessionContextTb4b4b4(Te6edf3generation Tff7b72= T79c0ff2LTb4b4b4, Te6edf3address Tff7b72= Te6edf3DEFAULT_ADDRESSTb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3staleFrameTb4b4b4)
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3staleFrameTb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3freshFrameTb4b4b4)
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3freshFrameTb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Regression test for a channel-buffer-replay bug: the production [RadioInterfaceService] buffers inbound bytes in
* a process-lifetime `Channel(UNLIMITED)`. Between `stop()` and the next `start()`, any bytes that arrive sit in
* the channel and would be replayed to the fresh collector — prepending stale packets to the next session's
* firmware handshake. `start()` must call [RadioInterfaceService.resetReceivedBuffer] before attaching the
* collector.
*/
Tf0883e@Test
Tff7b72fun Td2a8fftestStartDrainsReceivedBufferBeforeAttachingCollectorTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Tb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

T8b949e// resetReceivedBuffer must be invoked at least once per start() (twice total for two starts).
Te6edf3verifyTb4b4b4(Te6edf3atLeastTb4b4b4(T79c0ff2Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3resetReceivedBufferTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/** Additional regression: after many start/stop cycles, collectors must not accumulate. */
Tf0883e@Test
Tff7b72fun Td2a8fftestRepeatedStartStopDoesNotAccumulateCollectorsTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff8Tb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3receivedData Tff7b72= Te6edf3receivedDataTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)

Te6edf3repeatTb4b4b4(T79c0ff5Tb4b4b4) Tb4b4b4{
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3packet Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff4T79c0ff2Tb4b4b4)Tb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3packetTb4b4b4)

T8b949e// Despite six total start() calls, only the most recent collector is live.
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packetTb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Regression: when [RadioInterfaceService.currentDeviceAddressFlow] emits a new address mid-session (e.g. a late
* process-lifecycle address resolution on Android), the orchestrator must propagate it to [DatabaseManager] so Room
* writes land in the right per-device DB. Previously start() only switched the DB once via getDeviceAddress() and
* missed subsequent address changes, leaving the new session writing to the old DB.
*
* DatabaseManager is idempotent, so the redundant initial replay (matching the getDeviceAddress() snapshot) is a
* no-op; we therefore assert by argument value rather than brittle exact counts.
*/
Tf0883e@Test
Tff7b72fun Td2a8fftestCurrentDeviceAddressChangeSwitchesActiveDatabaseAfterStartTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3deviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Ta5d6ff"Ta5d6fftcp:192.168.1.100Ta5d6ff"Tb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3currentDeviceAddressFlow Tff7b72= Te6edf3deviceAddressFlowTb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

T8b949e// Initial address was switched (the first-valid emission in start() + the StateFlow
T8b949e// replay into the mid-session observer; DatabaseManager is idempotent for the dup).
Te6edf3verifySuspend Tb4b4b4{ Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6fftcp:192.168.1.100Ta5d6ff"Tb4b4b4) Tb4b4b4}

T8b949e// Mid-session address resolution must propagate to DatabaseManager.
Te6edf3deviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Ta5d6ff"Ta5d6fftcp:10.0.0.5Ta5d6ff"
Te6edf3verifySuspend Tb4b4b4{ Te6edf3databaseManagerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6fftcp:10.0.0.5Ta5d6ff"Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Lifecycle invariant: when the transport reports [ConnectionState.Connected] while the orchestrator is stopped
* (e.g. a late BLE liveness reconnect, or an emission that arrives after [MeshService.onDestroy]), the orchestrator
* MUST NOT auto-restart. The orchestrator is a Koin `@Single` whose lifetime exceeds the Android `Service`, so an
* unguarded observer launched in `init {}` would resurrect the orchestrator after a deliberate stop — collecting
* from [RadioInterfaceService.receivedData] in the background with no foreground service, no wake lock, and no UI.
*
* This also preserves the documented invariant that [MeshConnectionManagerImpl] is the only consumer of
* [RadioInterfaceService.connectionState]. Recovery after stop is the host's responsibility: it must call `start()`
* explicitly (e.g. via a fresh `MeshService.onCreate()`).
*
* Replaces the previous "orphan-Connected recovery" behavior that was removed for violating both invariants.
*/
Tf0883e@Test
Tff7b72fun Td2a8fftestConnectedWhileStoppedDoesNotRestartWithoutExplicitStartTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ReceivedRadioFrameTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff8Tb4b4b4)
Tff7b72val Te6edf3connectionState Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72= Te6edf3createOrchestratorTb4b4b4(Te6edf3receivedData Tff7b72= Te6edf3receivedDataTb4b4b4, Te6edf3connectionState Tff7b72= Te6edf3connectionStateTb4b4b4)
Te6edf3every Tb4b4b4{ Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNum Tb4b4b4} Te6edf3returns Te6edf3MutableStateFlowTb4b4b4(Tff7b72nullTb4b4b4)

T8b949e// Stopped; never call start() explicitly.
Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)

T8b949e// Transport reaches Connected — orchestrator must stay stopped.
Te6edf3connectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)

T8b949e// No collector may have been attached: a packet emitted now is unhandled.
Tff7b72val Te6edf3packet Tff7b72= Te6edf3frameTb4b4b4(Te6edf3byteArrayOfTb4b4b4(T79c0ff7Tb4b4b4, T79c0ff7Tb4b4b4, T79c0ff7Tb4b4b4)Tb4b4b4)
Te6edf3receivedDataTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3packetTb4b4b4)
Te6edf3verifySuspendTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3messageProcessorTb4b4b4.Te6edf3handleFromRadioTb4b4b4(Te6edf3packetTb4b4b4, Tff7b72nullTb4b4b4) Tb4b4b4}

T8b949e// Likewise, subsequent transport state cycles must not restart the orchestrator.
Te6edf3connectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected
Te6edf3connectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Te6edf3assertFalseTb4b4b4(Te6edf3orchestratorTb4b4b4.Te6edf3isRunningTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftcpReconnectWarnsAndStillConnectsWhenLocalNetworkAccessIsMissingTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
T8b949e// Android 17 gates the socket: without ACCESS_LOCAL_NETWORK a local TCP connect times out silently. The
T8b949e// service has no Activity to request from, so the contract is warn-and-proceed — proceed because the address
T8b949e// prefix says nothing about locality (public-IP/VPN radios need no permission) and because connect() is the
T8b949e// sole installer of the transport-recovery listeners.
T8b949e//
T8b949e// Pre-warm the resource: the first read completes on compose-resources' own Dispatchers.Default scope, so an
T8b949e// un-warmed lookup lands after the assertions (see ScannerViewModelTest's warmScanFailureStrings). The same
T8b949e// safeCatchingAll-with-fallback shape as production keeps the expected text identical either way.
Tff7b72val Te6edf3expectedMessage Tff7b72=
Te6edf3safeCatchingAll Tb4b4b4{ Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3local_network_permission_denied_hintTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3getOrDefaultTb4b4b4(Te6edf3UNTRANSLATED_LOCAL_NETWORK_PERMISSION_DENIED_HINTTb4b4b4)
Tff7b72val Te6edf3orchestrator Tff7b72=
Te6edf3createOrchestratorTb4b4b4(
Te6edf3currentDeviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.100Ta5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3localNetworkAccess Tff7b72= Te6edf3LocalNetworkAccess Tb4b4b4{ Tff7b72false Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3expectedMessageTb4b4b4, Te6edf3SeverityTb4b4b4.Te6edf3WarnTb4b4b4) Tb4b4b4}
Te6edf3verify Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8fftcpReconnectConnectsWithoutAWarningWhenLocalNetworkAccessIsHeldTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3orchestrator Tff7b72=
Te6edf3createOrchestratorTb4b4b4(
Te6edf3currentDeviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Ta5d6ff"Ta5d6fft192.168.1.100Ta5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3localNetworkAccess Tff7b72= Te6edf3LocalNetworkAccess Tb4b4b4{ Tff7b72true Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verifyTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffnonTcpReconnectIsUnaffectedByLocalNetworkAccessTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Guards against over-gating: ACCESS_LOCAL_NETWORK has nothing to do with a BLE radio, so a denied grant
T8b949e// must neither warn nor keep a Bluetooth device from reconnecting.
Tff7b72val Te6edf3orchestrator Tff7b72=
Te6edf3createOrchestratorTb4b4b4(
Te6edf3currentDeviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Te6edf3DEFAULT_ADDRESSTb4b4b4)Tb4b4b4,
Te6edf3localNetworkAccess Tff7b72= Te6edf3LocalNetworkAccess Tb4b4b4{ Tff7b72false Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)

Te6edf3verify Tb4b4b4{ Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3connectTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3verifyTb4b4b4(Te6edf3exactlyTb4b4b4(T79c0ff0Tb4b4b4)Tb4b4b4) Tb4b4b4{ Te6edf3serviceRepositoryTb4b4b4.Te6edf3setErrorMessageTb4b4b4(Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3anyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4}

Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.2s